<commit_before>/*
 * Copyright (C) 2017 TypeFox and others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 */

import { injectable, inject } from 'inversify';
import { Git } from '../common/git';
import { GIT_CONTEXT_MENU } from './git-context-menu';
import { GitFileChange, GitFileStatus, Repository, WorkingDirectoryStatus } from '../common/model';
import { GitWatcher, GitStatusChangeEvent } from '../common/git-watcher';
import { GIT_RESOURCE_SCHEME } from './git-resource';
import { GitRepositoryProvider } from './git-repository-provider';
import { MessageService, ResourceProvider, CommandService, DisposableCollection } from '@theia/core';
import URI from '@theia/core/lib/common/uri';
import { VirtualRenderer, VirtualWidget, ContextMenuRenderer, OpenerService, open } from '@theia/core/lib/browser';
import { h } from '@phosphor/virtualdom/lib';
import { Message } from '@phosphor/messaging';
import { DiffUris } from '@theia/editor/lib/browser/diff-uris';
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';

export interface GitFileChangeNode extends GitFileChange {
    readonly icon: string;
    readonly label: string;
    readonly description: string;
}

@injectable()
export class GitWidget extends VirtualWidget {

    protected stagedChanges: GitFileChangeNode[] = [];
    protected unstagedChanges: GitFileChangeNode[] = [];
    protected mergeChanges: GitFileChangeNode[] = [];
    protected message: string = '';
    protected messageInputHighlighted: boolean = false;
    protected additionalMessage: string = '';
    protected status: WorkingDirectoryStatus;
    protected toDispose = new DisposableCollection();

    constructor(
        @inject(Git) protected readonly git: Git,
        @inject(GitRepositoryProvider) protected readonly repositoryProvider: GitRepositoryProvider,
        @inject(GitWatcher) protected readonly gitWatcher: GitWatcher,
        @inject(OpenerService) protected readonly openerService: OpenerService,
        @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer,
        @inject(ResourceProvider) protected readonly resourceProvider: ResourceProvider,
        @inject(MessageService) protected readonly messageService: MessageService,
        @inject(LabelProvider) protected readonly labelProvider: LabelProvider,
        @inject(CommandService) protected readonly commandService: CommandService,
        @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService) {
        super();
        this.id = 'theia-gitContainer';
        this.title.label = 'Git';

        this.addClass('theia-git');
        this.update();

        this.repositoryProvider.onDidChangeRepository(repo => {
            this.initialize(repo);
        });
    }

    async initialize(repository: Repository | undefined): Promise<void> {
        if (repository) {
            this.toDispose.dispose();
            this.toDispose.push(await this.gitWatcher.watchGitChanges(repository));
            this.toDispose.push(this.gitWatcher.onGitEvent(async gitEvent => {
                if (GitStatusChangeEvent.is(gitEvent)) {
                    this.status = gitEvent.status;
                    this.updateView(gitEvent.status);
                }
            }));
        }
    }

    protected async updateView(status: WorkingDirectoryStatus | undefined) {
        this.stagedChanges = [];
        this.unstagedChanges = [];
        this.mergeChanges = [];
        if (status) {
            status.changes.forEach(async change => {
                const uri = new URI(change.uri);
                const repository = this.repositoryProvider.selectedRepository;
                const [icon, label, description] = await Promise.all([
                    this.labelProvider.getIcon(uri),
                    this.labelProvider.getName(uri),
                    repository ? this.getRepositoryRelativePath(repository, uri) : this.labelProvider.getLongName(uri)
                ]);
                if (GitFileStatus[GitFileStatus.Conflicted.valueOf()] !== GitFileStatus[change.status]) {
                    if (change.staged) {
                        this.stagedChanges.push({
                            icon, label, description,
                            ...change
                        });
                    } else {
                        this.unstagedChanges.push({
                            icon, label, description,
                            ...change
                        });
                    }
                } else {
                    if (!change.staged) {
                        this.mergeChanges.push({
                            icon, label, description,
                            ...change
                        });
                    }
                }
            });
        }
        this.update();
    }

    protected render(): h.Child {
        const repository = this.repositoryProvider.selectedRepository;

        const commandBar = this.renderCommandBar(repository);
        const messageInput = this.renderMessageInput();
        const messageTextarea = this.renderMessageTextarea();
        const headerContainer = h.div({ className: 'headerContainer' }, commandBar, messageInput, messageTextarea);

        const mergeChanges = this.renderMergeChanges(repository) || '';
        const stagedChanges = this.renderStagedChanges(repository) || '';
        const unstagedChanges = this.renderUnstagedChanges(repository) || '';
        const changesContainer = h.div({ className: "changesOuterContainer" }, mergeChanges, stagedChanges, unstagedChanges);

        return [headerContainer, changesContainer];
    }

    /**
     * After rendering the DOM elements, it makes sure that the selection (`selectionIndex`) is correct in the repositories
     * drop-down even if one adds/removes local Git clones to/from the workspace.
     *
     * By default the `selectionIndex` is `0`, so we need to set it based on the user's repository selection.<commit_msg>[git] fix concurrency issue with git widgets update (fixes #871)<commit_after>/*
 * Copyright (C) 2017 TypeFox and others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 */

import { injectable, inject } from 'inversify';
import { Git } from '../common/git';
import { GIT_CONTEXT_MENU } from './git-context-menu';
import { GitFileChange, GitFileStatus, Repository, WorkingDirectoryStatus } from '../common/model';
import { GitWatcher, GitStatusChangeEvent } from '../common/git-watcher';
import { GIT_RESOURCE_SCHEME } from './git-resource';
import { GitRepositoryProvider } from './git-repository-provider';
import { MessageService, ResourceProvider, CommandService, DisposableCollection } from '@theia/core';
import URI from '@theia/core/lib/common/uri';
import { VirtualRenderer, VirtualWidget, ContextMenuRenderer, OpenerService, open } from '@theia/core/lib/browser';
import { h } from '@phosphor/virtualdom/lib';
import { Message } from '@phosphor/messaging';
import { DiffUris } from '@theia/editor/lib/browser/diff-uris';
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';

export interface GitFileChangeNode extends GitFileChange {
    readonly icon: string;
    readonly label: string;
    readonly description: string;
}

@injectable()
export class GitWidget extends VirtualWidget {

    protected stagedChanges: GitFileChangeNode[] = [];
    protected unstagedChanges: GitFileChangeNode[] = [];
    protected mergeChanges: GitFileChangeNode[] = [];
    protected message: string = '';
    protected messageInputHighlighted: boolean = false;
    protected additionalMessage: string = '';
    protected status: WorkingDirectoryStatus;
    protected toDispose = new DisposableCollection();

    constructor(
        @inject(Git) protected readonly git: Git,
        @inject(GitRepositoryProvider) protected readonly repositoryProvider: GitRepositoryProvider,
        @inject(GitWatcher) protected readonly gitWatcher: GitWatcher,
        @inject(OpenerService) protected readonly openerService: OpenerService,
        @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer,
        @inject(ResourceProvider) protected readonly resourceProvider: ResourceProvider,
        @inject(MessageService) protected readonly messageService: MessageService,
        @inject(LabelProvider) protected readonly labelProvider: LabelProvider,
        @inject(CommandService) protected readonly commandService: CommandService,
        @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService) {
        super();
        this.id = 'theia-gitContainer';
        this.title.label = 'Git';

        this.addClass('theia-git');
        this.update();

        this.repositoryProvider.onDidChangeRepository(repo => {
            this.initialize(repo);
        });
    }

    async initialize(repository: Repository | undefined): Promise<void> {
        if (repository) {
            this.toDispose.dispose();
            this.toDispose.push(await this.gitWatcher.watchGitChanges(repository));
            this.toDispose.push(this.gitWatcher.onGitEvent(async gitEvent => {
                if (GitStatusChangeEvent.is(gitEvent)) {
                    this.status = gitEvent.status;
                    this.updateView(gitEvent.status);
                }
            }));
        }
    }

    protected async updateView(status: WorkingDirectoryStatus | undefined) {
        this.stagedChanges = [];
        this.unstagedChanges = [];
        this.mergeChanges = [];
        if (status) {
            for (const change of status.changes) {
                const uri = new URI(change.uri);
                const repository = this.repositoryProvider.selectedRepository;
                const [icon, label, description] = await Promise.all([
                    this.labelProvider.getIcon(uri),
                    this.labelProvider.getName(uri),
                    repository ? this.getRepositoryRelativePath(repository, uri) : this.labelProvider.getLongName(uri)
                ]);
                if (GitFileStatus[GitFileStatus.Conflicted.valueOf()] !== GitFileStatus[change.status]) {
                    if (change.staged) {
                        this.stagedChanges.push({
                            icon, label, description,
                            ...change
                        });
                    } else {
                        this.unstagedChanges.push({
                            icon, label, description,
                            ...change
                        });
                    }
                } else {
                    if (!change.staged) {
                        this.mergeChanges.push({
                            icon, label, description,
                            ...change
                        });
                    }
                }
            }
        }
        this.update();
    }

    protected render(): h.Child {
        const repository = this.repositoryProvider.selectedRepository;

        const commandBar = this.renderCommandBar(repository);
        const messageInput = this.renderMessageInput();
        const messageTextarea = this.renderMessageTextarea();
        const headerContainer = h.div({ className: 'headerContainer' }, commandBar, messageInput, messageTextarea);

        const mergeChanges = this.renderMergeChanges(repository) || '';
        const stagedChanges = this.renderStagedChanges(repository) || '';
        const unstagedChanges = this.renderUnstagedChanges(repository) || '';
        const changesContainer = h.div({ className: "changesOuterContainer" }, mergeChanges, stagedChanges, unstagedChanges);

        return [headerContainer, changesContainer];
    }

    /**
     * After rendering the DOM elements, it makes sure that the selection (`selectionIndex`) is correct in the repositories
     * drop-down even if one adds/removes local Git clones to/from the workspace.
     *
     * By default the `selectionIndex` is `0`, so we need to set it based on the user's repository selection.
{'fraction_non_alphanumeric': 0.10014900792094737, 'fraction_numerical': 0.0016469296525762685, 'mean_word_length': 2.545176536002224, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 10, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3615199', 'n_tokens_mistral': 3147, 'n_tokens_neox': 3024, 'n_words': 775}