Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show file changes as a tree in Source Control view #7505

Merged
merged 1 commit into from
May 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

## v1.2.0

Breaking changes:

- [scm] support file tree mode in Source Control view. Classes that extend ScmWidget will likely require changes [#7505](https://github.com/eclipse-theia/theia/pull/7505)

## v1.1.0

- [application-manager] added meta tag to enable fullscreen on iOS devices [#7663](https://github.com/eclipse-theia/theia/pull/7663)
Expand Down
67 changes: 53 additions & 14 deletions packages/git/src/browser/git-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { WorkspaceService } from '@theia/workspace/lib/browser';
import { GitRepositoryProvider } from './git-repository-provider';
import { GitErrorHandler } from '../browser/git-error-handler';
import { ScmWidget } from '@theia/scm/lib/browser/scm-widget';
import { ScmTreeWidget } from '@theia/scm/lib/browser/scm-tree-widget';
import { ScmResource, ScmCommand } from '@theia/scm/lib/browser/scm-provider';
import { ProgressService } from '@theia/core/lib/common/progress-service';
import { GitPreferences } from './git-preferences';
Expand Down Expand Up @@ -225,8 +226,8 @@ export class GitContribution implements CommandContribution, MenuContribution, T
});

const registerResourceAction = (group: string, action: MenuAction) => {
menus.registerMenuAction(ScmWidget.RESOURCE_INLINE_MENU, action);
menus.registerMenuAction([...ScmWidget.RESOURCE_CONTEXT_MENU, group], action);
menus.registerMenuAction(ScmTreeWidget.RESOURCE_INLINE_MENU, action);
menus.registerMenuAction([...ScmTreeWidget.RESOURCE_CONTEXT_MENU, group], action);
};

registerResourceAction('navigation', {
Expand Down Expand Up @@ -264,9 +265,37 @@ export class GitContribution implements CommandContribution, MenuContribution, T
when: 'scmProvider == git && scmResourceGroup == merge'
});

const registerResourceFolderAction = (group: string, action: MenuAction) => {
menus.registerMenuAction(ScmTreeWidget.RESOURCE_FOLDER_INLINE_MENU, action);
menus.registerMenuAction([...ScmTreeWidget.RESOURCE_FOLDER_CONTEXT_MENU, group], action);
};

registerResourceFolderAction('1_modification', {
commandId: GIT_COMMANDS.DISCARD.id,
when: 'scmProvider == git && scmResourceGroup == workingTree'
});
registerResourceFolderAction('1_modification', {
commandId: GIT_COMMANDS.STAGE.id,
when: 'scmProvider == git && scmResourceGroup == workingTree'
});

registerResourceFolderAction('1_modification', {
commandId: GIT_COMMANDS.UNSTAGE.id,
when: 'scmProvider == git && scmResourceGroup == index'
});

registerResourceFolderAction('1_modification', {
commandId: GIT_COMMANDS.DISCARD.id,
when: 'scmProvider == git && scmResourceGroup == merge'
});
registerResourceFolderAction('1_modification', {
commandId: GIT_COMMANDS.STAGE.id,
when: 'scmProvider == git && scmResourceGroup == merge'
});

const registerResourceGroupAction = (group: string, action: MenuAction) => {
menus.registerMenuAction(ScmWidget.RESOURCE_GROUP_INLINE_MENU, action);
menus.registerMenuAction([...ScmWidget.RESOURCE_GROUP_CONTEXT_MENU, group], action);
menus.registerMenuAction(ScmTreeWidget.RESOURCE_GROUP_INLINE_MENU, action);
menus.registerMenuAction([...ScmTreeWidget.RESOURCE_GROUP_CONTEXT_MENU, group], action);
};

registerResourceGroupAction('1_modification', {
Expand Down Expand Up @@ -382,34 +411,44 @@ export class GitContribution implements CommandContribution, MenuContribution, T
isEnabled: () => !!this.repositoryTracker.selectedRepository
});
registry.registerCommand(GIT_COMMANDS.UNSTAGE, {
execute: (arg: string | ScmResource) => {
const uri = typeof arg === 'string' ? arg : arg.sourceUri.toString();
execute: (arg: string | ScmResource[] | ScmResource) => {
const uris =
typeof arg === 'string' ? [ arg ] :
Array.isArray(arg) ? arg.map(r => r.sourceUri.toString()) :
[ arg.sourceUri.toString() ];
const provider = this.repositoryProvider.selectedScmProvider;
return provider && this.withProgress(() => provider.unstage(uri));
return provider && this.withProgress(() => provider.unstage(uris));
},
isEnabled: () => !!this.repositoryProvider.selectedScmProvider
});
registry.registerCommand(GIT_COMMANDS.STAGE, {
execute: (arg: string | ScmResource) => {
const uri = typeof arg === 'string' ? arg : arg.sourceUri.toString();
execute: (arg: string | ScmResource[] | ScmResource) => {
const uris =
typeof arg === 'string' ? [ arg ] :
Array.isArray(arg) ? arg.map(r => r.sourceUri.toString()) :
[ arg.sourceUri.toString() ];
const provider = this.repositoryProvider.selectedScmProvider;
return provider && this.withProgress(() => provider.stage(uri));
return provider && this.withProgress(() => provider.stage(uris));
},
isEnabled: () => !!this.repositoryProvider.selectedScmProvider
});
registry.registerCommand(GIT_COMMANDS.DISCARD, {
execute: (arg: string | ScmResource) => {
const uri = typeof arg === 'string' ? arg : arg.sourceUri.toString();
execute: (arg: string | ScmResource[] | ScmResource) => {
const uris =
typeof arg === 'string' ? [ arg ] :
Array.isArray(arg) ? arg.map(r => r.sourceUri.toString()) :
[ arg.sourceUri.toString() ];
const provider = this.repositoryProvider.selectedScmProvider;
return provider && this.withProgress(() => provider.discard(uri));
return provider && this.withProgress(() => provider.discard(uris));
},
isEnabled: () => !!this.repositoryProvider.selectedScmProvider
});
registry.registerCommand(GIT_COMMANDS.OPEN_CHANGED_FILE, {
execute: (arg: string | ScmResource) => {
const uri = typeof arg === 'string' ? new URI(arg) : arg.sourceUri;
this.editorManager.open(uri, { mode: 'reveal' });
}
},
isVisible: (arg: string | ScmResource, isFolder: boolean) => !isFolder
});
registry.registerCommand(GIT_COMMANDS.STASH, {
execute: () => this.quickOpenService.stash(),
Expand Down
93 changes: 71 additions & 22 deletions packages/git/src/browser/git-scm-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,14 +255,21 @@ export class GitScmProvider implements ScmProvider {
this.gitErrorHandler.handleError(error);
}
}
async stage(uri: string): Promise<void> {
async stage(uriArg: string | string[]): Promise<void> {
try {
const { repository, unstagedChanges, mergeChanges } = this;
const hasUnstagedChanges = unstagedChanges.some(change => change.uri === uri) || mergeChanges.some(change => change.uri === uri);
if (hasUnstagedChanges) {
const uris = Array.isArray(uriArg) ? uriArg : [ uriArg ];
const unstagedUris = uris
.filter(uri => {
const resourceUri = new URI(uri);
return unstagedChanges.some(change => resourceUri.isEqualOrParent(new URI(change.uri)))
|| mergeChanges.some(change => resourceUri.isEqualOrParent(new URI(change.uri)));
}
);
if (unstagedUris.length !== 0) {
// TODO resolve deletion conflicts
// TODO confirm staging of a unresolved file
await this.git.add(repository, uri);
await this.git.add(repository, uris);
}
} catch (error) {
this.gitErrorHandler.handleError(error);
Expand All @@ -278,11 +285,18 @@ export class GitScmProvider implements ScmProvider {
this.gitErrorHandler.handleError(error);
}
}
async unstage(uri: string): Promise<void> {
async unstage(uriArg: string | string[]): Promise<void> {
try {
const { repository, stagedChanges } = this;
if (stagedChanges.some(change => change.uri === uri)) {
await this.git.unstage(repository, uri);
const uris = Array.isArray(uriArg) ? uriArg : [ uriArg ];
const stagedUris = uris
.filter(uri => {
const resourceUri = new URI(uri);
return stagedChanges.some(change => resourceUri.isEqualOrParent(new URI(change.uri)));
}
);
if (stagedUris.length !== 0) {
await this.git.unstage(repository, uris);
}
} catch (error) {
this.gitErrorHandler.handleError(error);
Expand All @@ -303,31 +317,66 @@ export class GitScmProvider implements ScmProvider {
}
}
}
async discard(uri: string): Promise<void> {
async discard(uriArg: string | string[]): Promise<void> {
const { repository } = this;
const uris = Array.isArray(uriArg) ? uriArg : [ uriArg ];

const status = this.getStatus();
if (!(status && status.changes.some(change => change.uri === uri))) {
if (!status) {
return;
}
// Allow deletion, only iff the same file is not yet in the Git index.
if (await this.git.lsFiles(repository, uri, { errorUnmatch: true })) {
if (await this.confirm(uri)) {
try {
await this.git.unstage(repository, uri, { treeish: 'HEAD', reset: 'working-tree' });
} catch (error) {
this.gitErrorHandler.handleError(error);
}

const pairs = await Promise.all(
uris
.filter(uri => {
const uriAsUri = new URI(uri);
return status.changes.some(change => uriAsUri.isEqualOrParent(new URI(change.uri)));
})
.map(uri => {
const includeIndexFlag = async () => {
// Allow deletion, only iff the same file is not yet in the Git index.
const isInIndex = await this.git.lsFiles(repository, uri, { errorUnmatch: true });
return { uri, isInIndex };
};
return includeIndexFlag();
})
);

const urisInIndex = pairs.filter(pair => pair.isInIndex).map(pair => pair.uri);
if (urisInIndex.length !== 0) {
if (!await this.confirm(urisInIndex)) {
return;
}
} else {
await this.commands.executeCommand(WorkspaceCommands.FILE_DELETE.id, new URI(uri));
}

await Promise.all(
pairs.map(pair => {
const discardSingle = async () => {
if (pair.isInIndex) {
try {
await this.git.unstage(repository, pair.uri, { treeish: 'HEAD', reset: 'working-tree' });
} catch (error) {
this.gitErrorHandler.handleError(error);
}
} else {
await this.commands.executeCommand(WorkspaceCommands.FILE_DELETE.id, new URI(pair.uri));
}
};
return discardSingle();
})
);
}

protected confirm(path: string): Promise<boolean | undefined> {
const uri = new URI(path);
protected confirm(paths: string[]): Promise<boolean | undefined> {
let fileText: string;
if (paths.length <= 3) {
fileText = paths.map(path => new URI(path).displayName).join(', ');
} else {
fileText = `${paths.length} files`;
}
return new ConfirmDialog({
title: 'Discard changes',
msg: `Do you really want to discard changes in ${uri.displayName}?`
msg: `Do you really want to discard changes in ${fileText}?`
}).open();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { DebugStackFramesWidget } from '@theia/debug/lib/browser/view/debug-stac
import { DebugThreadsWidget } from '@theia/debug/lib/browser/view/debug-threads-widget';
import { TreeWidgetSelection } from '@theia/core/lib/browser/tree/tree-widget-selection';
import { ScmWidget } from '@theia/scm/lib/browser/scm-widget';
import { ScmTreeWidget } from '@theia/scm/lib/browser/scm-tree-widget';
import { ScmService } from '@theia/scm/lib/browser/scm-service';
import { ScmRepository } from '@theia/scm/lib/browser/scm-repository';
import { PluginScmProvider, PluginScmResourceGroup, PluginScmResource } from '../scm-main';
Expand Down Expand Up @@ -131,13 +132,19 @@ export class MenusContributionPointHandler {
} else if (location === 'scm/resourceGroup/context') {
for (const menu of allMenus[location]) {
const inline = menu.group && /^inline/.test(menu.group) || false;
const menuPath = inline ? ScmWidget.RESOURCE_GROUP_INLINE_MENU : ScmWidget.RESOURCE_GROUP_CONTEXT_MENU;
const menuPath = inline ? ScmTreeWidget.RESOURCE_GROUP_INLINE_MENU : ScmTreeWidget.RESOURCE_GROUP_CONTEXT_MENU;
toDispose.push(this.registerScmMenuAction(menuPath, menu));
}
} else if (location === 'scm/resourceFolder/context') {
akosyakov marked this conversation as resolved.
Show resolved Hide resolved
for (const menu of allMenus[location]) {
const inline = menu.group && /^inline/.test(menu.group) || false;
const menuPath = inline ? ScmTreeWidget.RESOURCE_FOLDER_INLINE_MENU : ScmTreeWidget.RESOURCE_FOLDER_CONTEXT_MENU;
toDispose.push(this.registerScmMenuAction(menuPath, menu));
}
} else if (location === 'scm/resourceState/context') {
for (const menu of allMenus[location]) {
const inline = menu.group && /^inline/.test(menu.group) || false;
const menuPath = inline ? ScmWidget.RESOURCE_INLINE_MENU : ScmWidget.RESOURCE_CONTEXT_MENU;
const menuPath = inline ? ScmTreeWidget.RESOURCE_INLINE_MENU : ScmTreeWidget.RESOURCE_CONTEXT_MENU;
toDispose.push(this.registerScmMenuAction(menuPath, menu));
}
} else if (location === 'debug/callstack/context') {
Expand Down
5 changes: 2 additions & 3 deletions packages/scm/src/browser/scm-amend-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { ScmRepository } from './scm-repository';
import { ScmAmendSupport, ScmCommit } from './scm-provider';

export interface ScmAmendComponentProps {
id: string,
style: React.CSSProperties | undefined,
repository: ScmRepository,
scmAmendSupport: ScmAmendSupport,
Expand Down Expand Up @@ -288,7 +287,7 @@ export class ScmAmendComponent extends React.Component<ScmAmendComponentProps, S
};

return (
<div className={ScmAmendComponent.Styles.COMMIT_CONTAINER + ' no-select'} style={style} id={this.props.id}>
<div className={ScmAmendComponent.Styles.COMMIT_CONTAINER + ' no-select'} style={style}>
{
this.state.amendingCommits.length > 0 || (this.state.lastCommit && this.state.transition.state !== 'none' && this.state.transition.direction === 'down')
? this.renderAmendingCommits()
Expand All @@ -297,7 +296,7 @@ export class ScmAmendComponent extends React.Component<ScmAmendComponentProps, S
{
this.state.lastCommit ?
<div>
<div id='lastCommit' className='changesContainer'>
<div id='lastCommit' className='theia-scm-amend'>
<div className='theia-header scm-theia-header'>
HEAD Commit
</div>
Expand Down
86 changes: 86 additions & 0 deletions packages/scm/src/browser/scm-amend-widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/********************************************************************************
* 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, inject } from 'inversify';
import { Message } from '@phosphor/messaging';
import { SelectionService } from '@theia/core/lib/common';
import * as React from 'react';
import {
ContextMenuRenderer, ReactWidget, LabelProvider, KeybindingRegistry, StorageService
} from '@theia/core/lib/browser';
import { ScmService } from './scm-service';
import { ScmAvatarService } from './scm-avatar-service';
import { ScmAmendComponent } from './scm-amend-component';

@injectable()
export class ScmAmendWidget extends ReactWidget {

static ID = 'scm-amend-widget';

@inject(ScmService) protected readonly scmService: ScmService;
@inject(ScmAvatarService) protected readonly avatarService: ScmAvatarService;
@inject(StorageService) protected readonly storageService: StorageService;
@inject(SelectionService) protected readonly selectionService: SelectionService;
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
@inject(KeybindingRegistry) protected readonly keybindings: KeybindingRegistry;

protected shouldScrollToRow = true;

constructor(
@inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer,
) {
super();
this.scrollOptions = {
suppressScrollX: true,
minScrollbarLength: 35
};
this.addClass('theia-scm-commit-container');
this.id = ScmAmendWidget.ID;
}

protected onUpdateRequest(msg: Message): void {
if (!this.isAttached || !this.isVisible) {
return;
}
super.onUpdateRequest(msg);
}

protected render(): React.ReactNode {
const repository = this.scmService.selectedRepository;
if (repository && repository.provider.amendSupport) {
return React.createElement(
ScmAmendComponent,
{
key: `amend:${repository.provider.rootUri}`,
style: { flexGrow: 0 },
repository: repository,
scmAmendSupport: repository.provider.amendSupport,
setCommitMessage: this.setInputValue,
avatarService: this.avatarService,
storageService: this.storageService,
}
);
}
}

protected setInputValue = (event: React.FormEvent<HTMLTextAreaElement> | React.ChangeEvent<HTMLTextAreaElement> | string) => {
const repository = this.scmService.selectedRepository;
if (repository) {
repository.input.value = typeof event === 'string' ? event : event.currentTarget.value;
}
};

}
Loading