Git - add file decoration provider for incoming changes (#204919)

* Initial implementation of a file decoration provider and quick diff provider

* Refactor file decoration provider

* Add incomingChanges to history provider

* Move decoration provider

* Move things around

* Add separate color for renamed incoming change

* Remove include that is not needed
This commit is contained in:
Ladislau Szomoru
2024-02-11 07:39:43 +01:00
committed by GitHub
parent 2aae82a102
commit c19383a66d
4 changed files with 173 additions and 18 deletions

View File

@@ -3,13 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor } from 'vscode';
import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor, l10n } from 'vscode';
import * as path from 'path';
import { Repository, GitResourceGroup } from './repository';
import { Model } from './model';
import { debounce } from './decorators';
import { filterEvent, dispose, anyEvent, fireEvent, PromiseSource } from './util';
import { GitErrorCodes, Status } from './api/git';
import { filterEvent, dispose, anyEvent, fireEvent, PromiseSource, combinedDisposable } from './util';
import { Change, GitErrorCodes, Status } from './api/git';
class GitIgnoreDecorationProvider implements FileDecorationProvider {
@@ -153,6 +153,100 @@ class GitDecorationProvider implements FileDecorationProvider {
}
}
class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider {
private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
readonly onDidChangeFileDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;
private decorations = new Map<string, FileDecoration>();
private readonly disposables: Disposable[] = [];
constructor(private readonly repository: Repository) {
this.disposables.push(window.registerFileDecorationProvider(this));
repository.historyProvider.onDidChangeCurrentHistoryItemGroupBase(this.onDidChangeCurrentHistoryItemGroupBase, this, this.disposables);
}
private async onDidChangeCurrentHistoryItemGroupBase(): Promise<void> {
const newDecorations = new Map<string, FileDecoration>();
await this.collectIncomingChangesFileDecorations(newDecorations);
const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()]));
this.decorations = newDecorations;
this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true)));
}
private async collectIncomingChangesFileDecorations(bucket: Map<string, FileDecoration>): Promise<void> {
for (const change of await this.getIncomingChanges()) {
switch (change.status) {
case Status.INDEX_ADDED:
bucket.set(change.uri.toString(), {
badge: '↓A',
color: new ThemeColor('gitDecoration.incomingAddedForegroundColor'),
tooltip: l10n.t('Incoming Changes (added)'),
});
break;
case Status.DELETED:
bucket.set(change.uri.toString(), {
badge: '↓D',
color: new ThemeColor('gitDecoration.incomingDeletedForegroundColor'),
tooltip: l10n.t('Incoming Changes (deleted)'),
});
break;
case Status.INDEX_RENAMED:
bucket.set(change.originalUri.toString(), {
badge: '↓R',
color: new ThemeColor('gitDecoration.incomingRenamedForegroundColor'),
tooltip: l10n.t('Incoming Changes (renamed)'),
});
break;
case Status.MODIFIED:
bucket.set(change.uri.toString(), {
badge: '↓M',
color: new ThemeColor('gitDecoration.incomingModifiedForegroundColor'),
tooltip: l10n.t('Incoming Changes (modified)'),
});
break;
default: {
bucket.set(change.uri.toString(), {
badge: '↓~',
color: new ThemeColor('gitDecoration.incomingModifiedForegroundColor'),
tooltip: l10n.t('Incoming Changes'),
});
break;
}
}
}
}
private async getIncomingChanges(): Promise<Change[]> {
try {
const historyProvider = this.repository.historyProvider;
const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup;
if (!currentHistoryItemGroup?.base) {
return [];
}
const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base.id);
if (!ancestor) {
return [];
}
const changes = await this.repository.diffBetween(ancestor.id, currentHistoryItemGroup.base.id);
return changes;
} catch (err) {
return [];
}
}
provideFileDecoration(uri: Uri): FileDecoration | undefined {
return this.decorations.get(uri.toString());
}
dispose(): void {
dispose(this.disposables);
}
}
export class GitDecorations {
@@ -191,8 +285,12 @@ export class GitDecorations {
}
private onDidOpenRepository(repository: Repository): void {
const provider = new GitDecorationProvider(repository);
this.providers.set(repository, provider);
const providers = combinedDisposable([
new GitDecorationProvider(repository),
new GitIncomingChangesFileDecorationProvider(repository)
]);
this.providers.set(repository, providers);
}
private onDidCloseRepository(repository: Repository): void {

View File

@@ -6,7 +6,7 @@
import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel } from 'vscode';
import { Repository, Resource } from './repository';
import { IDisposable, filterEvent } from './util';
import { IDisposable, dispose, filterEvent } from './util';
import { toGitUri } from './uri';
import { Branch, RefType, UpstreamRef } from './api/git';
import { emojify, ensureEmojis } from './emoji';
@@ -17,16 +17,20 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
private readonly _onDidChangeCurrentHistoryItemGroup = new EventEmitter<void>();
readonly onDidChangeCurrentHistoryItemGroup: Event<void> = this._onDidChangeCurrentHistoryItemGroup.event;
private readonly _onDidChangeCurrentHistoryItemGroupBase = new EventEmitter<void>();
readonly onDidChangeCurrentHistoryItemGroupBase: Event<void> = this._onDidChangeCurrentHistoryItemGroupBase.event;
private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
readonly onDidChangeFileDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;
private _HEAD: Branch | undefined;
private _currentHistoryItemGroup: SourceControlHistoryItemGroup | undefined;
get currentHistoryItemGroup(): SourceControlHistoryItemGroup | undefined { return this._currentHistoryItemGroup; }
set currentHistoryItemGroup(value: SourceControlHistoryItemGroup | undefined) {
this._currentHistoryItemGroup = value;
this._onDidChangeCurrentHistoryItemGroup.fire();
this.logger.trace('GitHistoryProvider:onDidRunGitStatus - currentHistoryItemGroup:', JSON.stringify(value));
}
private historyItemDecorations = new Map<string, FileDecoration>();
@@ -55,26 +59,35 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
return;
}
this._HEAD = this.repository.HEAD;
// Check if HEAD does not support incoming/outgoing (detached commit, tag)
if (!this._HEAD?.name || !this._HEAD?.commit || this._HEAD.type === RefType.Tag) {
this.currentHistoryItemGroup = undefined;
if (!this.repository.HEAD?.name || !this.repository.HEAD?.commit || this.repository.HEAD.type === RefType.Tag) {
this.logger.trace('GitHistoryProvider:onDidRunGitStatus - HEAD does not support incoming/outgoing');
this.currentHistoryItemGroup = undefined;
this._HEAD = this.repository.HEAD;
return;
}
this.currentHistoryItemGroup = {
id: `refs/heads/${this._HEAD.name ?? ''}`,
label: this._HEAD.name ?? '',
base: this._HEAD.upstream ?
id: `refs/heads/${this.repository.HEAD.name ?? ''}`,
label: this.repository.HEAD.name ?? '',
base: this.repository.HEAD.upstream ?
{
id: `refs/remotes/${this._HEAD.upstream.remote}/${this._HEAD.upstream.name}`,
label: `${this._HEAD.upstream.remote}/${this._HEAD.upstream.name}`,
id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`,
label: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`,
} : undefined
};
this.logger.trace('GitHistoryProvider:onDidRunGitStatus - currentHistoryItemGroup:', JSON.stringify(this.currentHistoryItemGroup));
// Check if Upstream has changed
if (force ||
this._HEAD?.upstream?.name !== this.repository.HEAD?.upstream?.name ||
this._HEAD?.upstream?.remote === this.repository.HEAD?.upstream?.remote ||
this._HEAD?.upstream?.commit === this.repository.HEAD?.upstream?.commit) {
this.logger.trace('GitHistoryProvider:onDidRunGitStatus - Upstream has changed');
this._onDidChangeCurrentHistoryItemGroupBase.fire();
}
this._HEAD = this.repository.HEAD;
}
async provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions): Promise<SourceControlHistoryItem[]> {
@@ -216,6 +229,6 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
dispose(this.disposables);
}
}